package com.gogant.spider.http;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import javax.swing.text.AttributeSet;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTML.Attribute;
import javax.swing.text.html.HTML.Tag;
import javax.swing.text.html.HTMLEditorKit.ParserCallback;

public final class HtmlSelector extends ParserCallback {
	private PathNode[] nodePath;
	private List<TempPath> matchList = new ArrayList<TempPath>();
	private List<HtmlNode> nodeList = new ArrayList<HtmlNode>();
	private boolean selectHtml;
	private boolean selectChildText;

	public static HtmlSelector[] create(String... paths) {
		if (paths == null) {
			return null;
		}
		HtmlSelector[] selectors = new HtmlSelector[paths.length];
		for (int i = 0; i < paths.length; i++) {
			selectors[i] = new HtmlSelector(paths[i]);
		}
		return selectors;
	}

	public static HtmlSelector clone(HtmlSelector selector) {
		if (selector == null) {
			return null;
		}
		HtmlSelector newSelector = new HtmlSelector();
		newSelector.nodePath = selector.nodePath;
		newSelector.selectChildText = selector.selectChildText;
		newSelector.selectHtml = selector.selectHtml;
		return newSelector;
	}

	public static HtmlSelector[] clone(HtmlSelector... selectors) {
		if (selectors == null) {
			return null;
		}
		HtmlSelector[] newSelectors = new HtmlSelector[selectors.length];
		for (int i = 0; i < selectors.length; i++) {
			newSelectors[i] = clone(selectors[i]);
		}
		return newSelectors;
	}

	private HtmlSelector() {
	}

	public HtmlSelector(String path, boolean selectHtml, boolean selectChildText) {
		this.nodePath = parsePath(path);
		if (nodePath == null || nodePath.length == 0) {
			throw new IllegalArgumentException("illegal path: " + path);
		}
		this.selectHtml = selectHtml;
		this.selectChildText = selectChildText;
	}

	public HtmlSelector(String path) {
		this(path, false, false);
	}

	public boolean isSelectHtml() {
		return selectHtml;
	}

	public void setSelectHtml(boolean selectHtml) {
		this.selectHtml = selectHtml;
	}

	public boolean isSelectChildText() {
		return selectChildText;
	}

	public void setSelectChildText(boolean selectChildText) {
		this.selectChildText = selectChildText;
	}

	public List<HtmlNode> getNodeList() {
		return nodeList;
	}

	public boolean hasMatchedNode() {
		return nodeList.size() > 0;
	}

	public HtmlNode getFirstNode() {
		return getHtmlNode(0);
	}

	public HtmlNode getLastNode() {
		return getHtmlNode(nodeList.size() - 1);
	}

	public HtmlNode getHtmlNode(int idx) {
		if (idx < 0 || idx >= nodeList.size()) {
			return null;
		}
		return nodeList.get(idx);
	}

	public int getNodeCount() {
		return nodeList.size();
	}

	public String getNodeText(int idx) {
		HtmlNode node = getHtmlNode(idx);
		return node == null ? null : node.getText();
	}

	public String getNodeTrimText(int idx) {
		HtmlNode node = getHtmlNode(idx);
		return node == null ? null : node.getTrimText();
	}

	public String getNodeHtml(int idx) {
		HtmlNode node = getHtmlNode(idx);
		return node == null ? null : node.getHtml();
	}

	public String getNodeAttribute(int idx, String name) {
		HtmlNode node = getHtmlNode(idx);
		return node == null ? null : node.getAttribute(name);
	}

	public List<String> getNodeTextList() {
		List<String> textList = new ArrayList<String>(nodeList.size());
		for (HtmlNode node : nodeList) {
			textList.add(node.getText());
		}
		return textList;
	}

	public List<String> getNodeTrimTextList() {
		List<String> textList = new ArrayList<String>(nodeList.size());
		for (HtmlNode node : nodeList) {
			textList.add(node.getTrimText());
		}
		return textList;
	}

	public List<String> getNodeHtmlList() {
		List<String> textList = new ArrayList<String>(nodeList.size());
		for (HtmlNode node : nodeList) {
			textList.add(node.getHtml());
		}
		return textList;
	}

	public List<String> getNodeAttributeList(String name) {
		List<String> textList = new ArrayList<String>(nodeList.size());
		for (HtmlNode node : nodeList) {
			String value = node.getAttribute(name);
			if (value != null) {
				textList.add(value);
			}
		}
		return textList;
	}

	public void reset() {
		nodeList.clear();
		matchList.clear();
	}

	private PathNode[] parsePath(String path) {
		if (path == null) {
			return null;
		}
		path = path.trim();
		String[] items = path.split("\\s*/\\s*");
		if (items.length == 0) {
			return null;
		}
		List<PathNode> nodeList = new ArrayList<PathNode>();
		for (String item : items) {
			if (item.length() == 0) {
				continue;
			}
			int idx = item.indexOf('#');
			if (idx == 0) {
				throw new IllegalArgumentException("illegal tag: " + item);
			}
			int tagIndex = -1;
			if (idx > 0) {
				try {
					tagIndex = Integer.valueOf(item.substring(idx + 1).trim());
				} catch (Exception e) {
					throw new IllegalArgumentException("illegal tag: " + item + ", contains invalid tag index");
				}
				item = item.substring(0, idx).trim();
			}
			idx = item.indexOf('?');
			if (idx == 0) {
				throw new IllegalArgumentException("illegal tag: " + item);
			}

			String tagName = null;
			String[] attrStrs = null;
			if (idx > 0) {
				tagName = item.substring(0, idx).trim();
				attrStrs = item.substring(idx + 1).trim().split("\\s*&\\s*");
			} else {
				tagName = item;
			}
			Tag tag = HTML.getTag(tagName.toLowerCase());
			if (tag == null) {
				throw new IllegalArgumentException("illegal tag: " + tagName);
			}
			NodeAttr[] attrs = null;
			if (attrStrs != null && attrStrs.length > 0) {
				List<NodeAttr> attrList = new ArrayList<NodeAttr>();
				for (String attrItem : attrStrs) {
					idx = attrItem.indexOf('=');
					if (idx == 0) {
						throw new IllegalArgumentException("illegal attribute: " + attrItem);
					}
					String attrName = null;
					String attrValue = null;
					if (idx > 0) {
						attrName = attrItem.substring(0, idx).trim();
						attrValue = attrItem.substring(idx + 1).trim();
					} else {
						attrName = attrItem;
					}
					Attribute attr = HTML.getAttributeKey(attrName);
					if (attr == null) {
						throw new IllegalArgumentException("illegal attribute: " + attrName);
					}
					attrList.add(new NodeAttr(attr, attrValue));
				}
				attrs = attrList.toArray(new NodeAttr[attrList.size()]);
			}
			nodeList.add(new PathNode(tag, attrs, tagIndex));
		}
		return nodeList.toArray(new PathNode[nodeList.size()]);
	}

	private class PathNode {
		Tag tag;
		NodeAttr[] attrs;
		String nodeStr;
		int index = -1;

		public PathNode(Tag tag, NodeAttr[] attrs, int index) {
			this.tag = tag;
			this.attrs = attrs;
			this.index = index;
		}

		@Override
		public String toString() {
			if (nodeStr == null) {
				nodeStr = tag.toString();
				if (attrs != null && attrs.length > 0) {
					nodeStr += "?";
					for (int i = 0; i < attrs.length; i++) {
						NodeAttr attr = attrs[i];
						if (i > 0) {
							nodeStr += "&";
						}
						nodeStr += attr;
					}
				}
				if (index >= 0) {
					nodeStr += "#" + index;
				}
			}
			return nodeStr;
		}
	}

	private class NodeAttr {
		Attribute attr;
		Object value;
		String attrStr;

		public NodeAttr(Attribute attr, Object value) {
			this.attr = attr;
			this.value = value;
		}

		@Override
		public String toString() {
			if (attrStr == null) {
				attrStr = attr + (value == null ? "" : ("=" + value.toString()));
			}
			return attrStr;
		}
	}

	public class HtmlNode {
		Tag tag;
		AttributeSet attrSet;
		String text;
		String html;
		int startPosition;
		int endPosition;
		StringBuilder textBuilder;
		StringBuilder htmlBuilder;

		private HtmlNode(Tag tag, AttributeSet attrSet, int pos) {
			this.tag = tag;
			this.attrSet = attrSet.copyAttributes();
			this.startPosition = pos;
		}

		public Tag getTag() {
			return tag;
		}

		public boolean isTag(String tagName) {
			return tag == HTML.getTag(tagName);
		}

		public String getAttribute(String name) {
			if (attrSet == null) {
				return null;
			}
			Attribute key = HTML.getAttributeKey(name);
			return key == null ? null : getAttribute(key);
		}

		public String getAttribute(Attribute attrKey) {
			if (attrSet == null) {
				return null;
			}
			Object value = attrSet.getAttribute(attrKey);
			return value == null ? null : value.toString();
		}

		public boolean hasAttribute() {
			return attrSet != null && attrSet.getAttributeCount() > 0;
		}

		public Enumeration<?> getAttributeNames() {
			return attrSet == null ? null : attrSet.getAttributeNames();
		}

		public String getText() {
			if (text == null && textBuilder != null) {
				text = textBuilder.toString();
				textBuilder = null;
			}
			return text;
		}

		public String getTrimText() {
			String text = getText();
			if (text == null) {
				return "";
			} else {
				return text.trim();
			}
		}

		public String getHtml() {
			if (html == null && htmlBuilder != null) {
				html = htmlBuilder.toString();
				htmlBuilder = null;
			}
			return html;
		}

		private StringBuilder getTextBuilder() {
			if (textBuilder == null) {
				textBuilder = new StringBuilder();
			}
			return textBuilder;
		}

		private StringBuilder getHtmlBuilder() {
			if (htmlBuilder == null) {
				htmlBuilder = new StringBuilder();
			}
			return htmlBuilder;
		}

		@Override
		public String toString() {
			return "[tag: " + tag + ", attrs: " + attrSet + ", text: " + getText() + "]";
		}

		public String toHtml() {
			StringBuilder sb = new StringBuilder();
			sb.append("<").append(tag);
			if (attrSet != null && attrSet.getAttributeCount() > 0) {
				for (Enumeration<?> enu = attrSet.getAttributeNames(); enu.hasMoreElements();) {
					Object Key = enu.nextElement();
					Object value = attrSet.getAttribute(Key);
					sb.append(" ").append(Key).append("=\"").append(value == null ? "" : value.toString()).append("\"");
				}
			}
			if (endPosition > 0) {
				sb.append(">");
				if (getHtml() != null) {
					sb.append(getHtml());
				} else if (getText() != null) {
					sb.append(getText());
				}
				sb.append("</").append(tag).append(">");
			} else {
				sb.append("/>");
			}
			return sb.toString();
		}
	}

	private class TempPath extends ParserCallback {
		int nodeIndex = 0;
		int tagIndex = 0;
		HtmlNode tempNode;
		int[] indexes = new int[nodePath.length];

		private boolean isPathNodeMatched(int nodeIndex, Tag t, MutableAttributeSet a) {
			PathNode node = nodePath[nodeIndex];
			if (isNodeMatched(node, t, a)) {
				int currentIndex = indexes[nodeIndex];
				indexes[nodeIndex] = currentIndex + 1;
				if (node.index >= 0 && currentIndex != node.index) {
					return false;
				} else {
					return true;
				}
			} else {
				return false;
			}
		}

		@Override
		public void handleSimpleTag(Tag t, MutableAttributeSet a, int pos) {
			if (selectHtml && nodeIndex == nodePath.length - 1 && tempNode != null) {
				appendHtml(tempNode, t, a, true);
			}
			if (tagIndex != nodeIndex || nodeIndex + 1 != nodePath.length - 1) {
				return;
			}
			if (!isPathNodeMatched(nodeIndex + 1, t, a)) {
				return;
			}
			nodeList.add(new HtmlNode(t, a, pos));
		}

		@Override
		public void handleStartTag(Tag t, MutableAttributeSet a, int pos) {
			if (tagIndex < 0) {
				if (isPathNodeMatched(0, t, a)) {
					tagIndex = 0;
					nodeIndex = 0;
					if (nodePath.length == 1) {
						tempNode = new HtmlNode(t, a, pos);
					} else {
						indexes[nodeIndex + 1] = 0;
					}
				}
				return;
			}
			tagIndex++;
			if (selectHtml && tempNode != null && nodeIndex == nodePath.length - 1) {
				appendHtml(tempNode, t, a, false);
			}
			if (tagIndex == nodeIndex + 1 && tagIndex < nodePath.length) {
				if (isPathNodeMatched(nodeIndex + 1, t, a)) {
					nodeIndex++;
					if (nodeIndex == nodePath.length - 1) {
						tempNode = new HtmlNode(t, a, pos);
					} else {
						indexes[nodeIndex + 1] = 0;
					}
				}
			}
		}

		@Override
		public void handleEndTag(Tag t, int pos) {
			if (tagIndex < 0) {
				return;
			}
			if (selectHtml && tempNode != null && nodeIndex == nodePath.length - 1 && tagIndex > nodeIndex) {
				appendHtml(tempNode, t, null, true);
			}
			if (tagIndex == nodeIndex && nodeIndex == nodePath.length - 1) {
				if (tempNode == null || tempNode.tag != t) {
					tempNode = null;
					nodeIndex = -1;
					tagIndex = -1;
					return;
				} else {
					tempNode.endPosition = pos;
					nodeList.add(tempNode);
					tempNode = null;
				}
			}
			tagIndex--;
			if (tagIndex < nodeIndex) {
				nodeIndex = tagIndex;
			}
		}

		@Override
		public void handleText(char[] data, int pos) {
			if (tempNode == null) {
				return;
			}
			if (tagIndex >= nodeIndex && nodeIndex == nodePath.length - 1) {
				if (selectHtml) {
					tempNode.getHtmlBuilder().append(data);
				}
				if (selectChildText || tagIndex == nodeIndex) {
					tempNode.getTextBuilder().append(data);
				}
			}
		}

		private void appendHtml(HtmlNode node, Tag t, MutableAttributeSet a, boolean isEnd) {
			if (a == null && isEnd) {
				node.getHtmlBuilder().append("</").append(t).append(">");
			} else {
				StringBuilder sb = tempNode.getHtmlBuilder();
				sb.append("<").append(t);
				if (a != null && a.getAttributeCount() > 0) {
					for (Enumeration<?> enu = a.getAttributeNames(); enu.hasMoreElements();) {
						Object Key = enu.nextElement();
						Object value = a.getAttribute(Key);
						sb.append(" ").append(Key).append("=\"").append(value == null ? "" : value.toString()).append("\"");
					}
				}
				if (isEnd) {
					sb.append("/>");
				} else {
					sb.append(">");
				}
			}
		}
	}

	private boolean isNodeMatched(PathNode node, Tag t, MutableAttributeSet a) {
		if (node.tag != t) {
			return false;
		}
		if (node.attrs != null) {
			for (NodeAttr na : node.attrs) {
				Object value = a.getAttribute(na.attr);
				if (value == null || (na.value != null && !na.value.equals(value))) {
					return false;
				}
			}
		}
		return true;
	}

	@Override
	public void handleSimpleTag(Tag t, MutableAttributeSet a, int pos) {
		if (nodePath.length == 1) {
			if (isNodeMatched(nodePath[0], t, a)) {
				nodeList.add(new HtmlNode(t, a, pos));
			}
		}
		if (matchList.size() > 0) {
			for (TempPath mp : matchList) {
				mp.handleSimpleTag(t, a, pos);
			}
		}
	}

	@Override
	public void handleStartTag(Tag t, MutableAttributeSet a, int pos) {
		if (matchList.size() > 0) {
			for (TempPath tp : matchList) {
				tp.handleStartTag(t, a, pos);
			}
		}
		if (isNodeMatched(nodePath[0], t, a)) {
			TempPath tp = new TempPath();
			if (nodePath.length == 1) {
				tp.tempNode = new HtmlNode(t, a, pos);
			}
			matchList.add(tp);
		}
	}

	@Override
	public void handleEndTag(Tag t, int pos) {
		if (matchList.size() == 0) {
			return;
		}
		for (int i = matchList.size() - 1; i >= 0; i--) {
			TempPath mp = matchList.get(i);
			mp.handleEndTag(t, pos);
			if (mp.tagIndex < 0) {
				matchList.remove(i);
			}
		}
	}

	@Override
	public void handleText(char[] data, int pos) {
		if (matchList.size() == 0) {
			return;
		}
		for (TempPath mp : matchList) {
			mp.handleText(data, pos);
		}
	}
}
